﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SharpCompress.Common;
using SharpCompress.Readers;

namespace SharpCompress.Archives
{
    public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtractionListener
        where TEntry : IArchiveEntry
        where TVolume : IVolume
    {
        private readonly LazyReadOnlyCollection<TVolume> lazyVolumes;
        private readonly LazyReadOnlyCollection<TEntry> lazyEntries;

        public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionBegin;
        public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionEnd;

        public event EventHandler<CompressedBytesReadEventArgs> CompressedBytesRead;
        public event EventHandler<FilePartExtractionBeginEventArgs> FilePartExtractionBegin;

        protected ReaderOptions ReaderOptions { get; }

        private bool disposed;

#if !NO_FILE
        internal AbstractArchive(ArchiveType type, FileInfo fileInfo, ReaderOptions readerOptions)
        {
            Type = type;
            if (!fileInfo.Exists)
            {
                throw new ArgumentException("File does not exist: " + fileInfo.FullName);
            }
            ReaderOptions = readerOptions;
            readerOptions.LeaveStreamOpen = false;
            lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(fileInfo));
            lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
        }


        protected abstract IEnumerable<TVolume> LoadVolumes(FileInfo file);
#endif

        internal AbstractArchive(ArchiveType type, IEnumerable<Stream> streams, ReaderOptions readerOptions)
        {
            Type = type;
            ReaderOptions = readerOptions;
            lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(streams.Select(CheckStreams)));
            lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
        }

        internal AbstractArchive(ArchiveType type)
        {
            Type = type;
            lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
            lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
        }

        public ArchiveType Type { get; }

        void IArchiveExtractionListener.FireEntryExtractionBegin(IArchiveEntry entry)
        {
            EntryExtractionBegin?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
        }

        void IArchiveExtractionListener.FireEntryExtractionEnd(IArchiveEntry entry)
        {
            EntryExtractionEnd?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
        }

        private static Stream CheckStreams(Stream stream)
        {
            if (!stream.CanSeek || !stream.CanRead)
            {
                throw new ArgumentException("Archive streams must be Readable and Seekable");
            }
            return stream;
        }

        /// <summary>
        /// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive.
        /// </summary>
        public virtual ICollection<TEntry> Entries { get { return lazyEntries; } }

        /// <summary>
        /// Returns an ReadOnlyCollection of all the RarArchiveVolumes across the one or many parts of the RarArchive.
        /// </summary>
        public ICollection<TVolume> Volumes { get { return lazyVolumes; } }

        /// <summary>
        /// The total size of the files compressed in the archive.
        /// </summary>
        public virtual long TotalSize { get { return Entries.Aggregate(0L, (total, cf) => total + cf.CompressedSize); } }

        /// <summary>
        /// The total size of the files as uncompressed in the archive.
        /// </summary>
        public virtual long TotalUncompressSize { get { return Entries.Aggregate(0L, (total, cf) => total + cf.Size); } }

        protected abstract IEnumerable<TVolume> LoadVolumes(IEnumerable<Stream> streams);
        protected abstract IEnumerable<TEntry> LoadEntries(IEnumerable<TVolume> volumes);

        IEnumerable<IArchiveEntry> IArchive.Entries { get { return Entries.Cast<IArchiveEntry>(); } }

        IEnumerable<IVolume> IArchive.Volumes { get { return lazyVolumes.Cast<IVolume>(); } }

        public virtual void Dispose()
        {
            if (!disposed)
            {
                lazyVolumes.ForEach(v => v.Dispose());
                lazyEntries.GetLoaded().Cast<Entry>().ForEach(x => x.Close());
                disposed = true;
            }
        }

        void IArchiveExtractionListener.EnsureEntriesLoaded()
        {
            lazyEntries.EnsureFullyLoaded();
            lazyVolumes.EnsureFullyLoaded();
        }

        void IExtractionListener.FireCompressedBytesRead(long currentPartCompressedBytes, long compressedReadBytes)
        {
            CompressedBytesRead?.Invoke(this, new CompressedBytesReadEventArgs
            {
                CurrentFilePartCompressedBytesRead = currentPartCompressedBytes,
                CompressedBytesRead = compressedReadBytes
            });
        }

        void IExtractionListener.FireFilePartExtractionBegin(string name, long size, long compressedSize)
        {
            FilePartExtractionBegin?.Invoke(this, new FilePartExtractionBeginEventArgs
            {
                CompressedSize = compressedSize,
                Size = size,
                Name = name
            });
        }

        /// <summary>
        /// Use this method to extract all entries in an archive in order.
        /// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be 
        /// extracted sequentially for the best performance.
        /// 
        /// This method will load all entry information from the archive.
        /// 
        /// WARNING: this will reuse the underlying stream for the archive.  Errors may 
        /// occur if this is used at the same time as other extraction methods on this instance.
        /// </summary>
        /// <returns></returns>
        public IReader ExtractAllEntries()
        {
            ((IArchiveExtractionListener)this).EnsureEntriesLoaded();
            return CreateReaderForSolidExtraction();
        }

        protected abstract IReader CreateReaderForSolidExtraction();

        /// <summary>
        /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
        /// </summary>
        public virtual bool IsSolid { get { return false; } }

        /// <summary>
        /// The archive can find all the parts of the archive needed to fully extract the archive.  This forces the parsing of the entire archive.
        /// </summary>
        public bool IsComplete
        {
            get
            {
                ((IArchiveExtractionListener)this).EnsureEntriesLoaded();
                return Entries.All(x => x.IsComplete);
            }
        }
    }
}